using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; namespace OpenTap.Plugins.PluginDevelopment.Advanced_Examples { // This example shows how a 'virtual' property can be attached to // any type. It supports serialization and showing it in a user interface // However, direct references to it is more complicated and requires // a reference to the DLL that defines. Additionally, the attached property // can be made internal, further restricting access to it. // This can be used as a useful way of limiting how it is being used. // // This requires the creation of a new ITypeDataProvider. Be careful when creating a TypeDataProvider // it is easy to 'brick' your installation if it throws exceptions or make it slow if your TypeDataProvider // is not optimized and does unnecessary work. // In this example, let's add a unique identifier to all instruments. // In the GUI there will be a Instrument ID field for all instruments. // In XML this will look like adwadwa public class InstrumentIDTypeDataProvider : IStackedTypeDataProvider { // This needs to be enabled from the example component settings. internal static bool Enabled; public ITypeData GetTypeData(string identifier, TypeDataProviderStack stack) { if (!Enabled) return null; if (identifier.StartsWith(InstrumentIDTypeData.pre)) { var inner = stack.GetTypeData(identifier.Substring(InstrumentIDTypeData.pre.Length)); return InstrumentIDTypeData.FromInnerType(inner); } return null; } public ITypeData GetTypeData(object obj, TypeDataProviderStack stack) { if (!Enabled) return null; if (obj is IInstrument) { var inner = stack.GetTypeData(obj); return InstrumentIDTypeData.FromInnerType(inner); } return null; } // Determined by calling PrintTypeDataProviders. public double Priority => 2; // uncomment these to see which type data providers exist //public InstrumentIDTypeDataProvider() //{ // if (printed) return; // printed = true; // avoid recursive loop // PrintTypeDataProviders(); //} //static bool printed = false; // To get an overview of the existing type data provider, we recommend calling this: // This will help figure out which Priority we should choose. static void PrintTypeDataProviders() { TraceSource log = Log.CreateSource("TypeData"); var items = TypeData.GetDerivedTypes() .Where(x => x.CanCreateInstance) .Select(x => x.CreateInstance()) .OfType() .ToArray(); foreach (var item in items) { log.Info("{0} : {1}", item, item.Priority); } } } /// /// Helper class for getting / setting the instrument ID /// public class InstrumentIDProperty { /// Gets the current ID of the instrument. public static string GetValue(IInstrument instrument) { return InstrumentIDTypeData.InstrumentIdMember.GetValue(instrument) as string; } /// Sets the current ID of the instrument. public static void SetValue(IInstrument instrument, string id) { InstrumentIDTypeData.InstrumentIdMember.SetValue(instrument, id); } } /// /// This is the extension to the existing instrument type /// class InstrumentIDTypeData : ITypeData { internal const string pre = "InstID:"; readonly ITypeData innerType; /// /// It is very important that the type provider can resolve types very fast, therefore we store the type in this cache. /// static readonly ConditionalWeakTable cache = new ConditionalWeakTable(); public static ITypeData FromInnerType(ITypeData innerType) { return cache.GetValue(innerType, t => new InstrumentIDTypeData(t)); } InstrumentIDTypeData(ITypeData innerType) { this.innerType = innerType; } /// We only need to define the ID member once. So here it is stored as a static value. /// This can also help improve performance. public static readonly InstrumentIDMemberData InstrumentIdMember = new InstrumentIDMemberData(TypeData.FromType(typeof(string)), "Instrument.ID", null, new DisplayAttribute("Instrument ID")); public IEnumerable Attributes => innerType.Attributes; public string Name => pre + innerType.Name; public IEnumerable GetMembers() { // return all the members of the inner type and add our instrument ID menber. return innerType.GetMembers().Concat(new[] {InstrumentIdMember}); } public IMemberData GetMember(string name) { // if name matches our instrument ID member, then we use that. Otherwise see if innerType has the member. if (name == InstrumentIdMember.Name) return InstrumentIdMember; return innerType.GetMember(name); } // Nothing special is needed to create an instance. public object CreateInstance(object[] arguments) { return innerType.CreateInstance(arguments); } // BaseType is a bit tricky here. We recommend using innerType because the extended type behaves // for all purposes like it's InnerType, except when accessing the additional members. public ITypeData BaseType => innerType; // It is best to forward inner types CanCreateInstance. public bool CanCreateInstance => innerType.CanCreateInstance; } class InstrumentIDMemberData : IMemberData { public InstrumentIDMemberData(ITypeData type, string name, object defaultValue, params object[] attributes) { this.Name = name; this.TypeDescriptor = type; DeclaringType = TypeData.FromType(typeof(IInstrument)); this.defaultValue = defaultValue; this.Attributes = attributes; } // The default attributes, in this case just a DisplayAttribute. public IEnumerable Attributes { get; } // The name of the property e.g "Instrument.ID" public string Name { get; } // This gets called whenever the value is set. public void SetValue(object owner, object value) { if (false == owner is IInstrument) throw new ArgumentException("First argument must be an instrument", nameof(owner)); if (value is string || value == null) { // With ConditionalWeakTable, you have to remove the value before inserting a new one. lock (valueTable) { valueTable.Remove(owner); valueTable.Add(owner, value); } } else { throw new ArgumentException("Value must be a string or null.", nameof(value)); } } // This is called when the value is gotten. public object GetValue(object owner) { if (owner is IInstrument) { lock (valueTable) return valueTable.GetValue(owner, x => defaultValue); } throw new ArgumentException("Argument must be an instrument."); } // The declaring type, in this case we just say it is IInstrument. It does not matter much. public ITypeData DeclaringType { get; } // For example, the type 'String'. public ITypeData TypeDescriptor { get; } // The property is writable. public bool Writable { get; } = true; // The property is readable. public bool Readable { get; } = true; // this table contains the actual values of the virtual property. // it is a ConditionalWeakTable so that the values will automatically get cleaned up when the object is // garbage collected. readonly ConditionalWeakTable valueTable = new ConditionalWeakTable(); // the default value, 'null'. readonly object defaultValue; } // A test step to test the property. [Display("Instrument ID Step", "Demonstrates how an Instrument ID can be fetched", Groups: new[] {"Examples", "Plugin Development", "Advanced Examples"})] public class InstrumentIDTestStep : TestStep { public IInstrument Instrument { get; set; } public override void Run() { var id = InstrumentIDProperty.GetValue(Instrument); Log.Info("Instrument has ID: {0}", id); } } }